﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Internal;
using Microsoft.Internal.Collections;
using System.Globalization;
using System.Diagnostics;

namespace System.ComponentModel.Composition
{
    // TODO-MT : this class appears to have some thread safety, but that's largely fiction
    // We protect members and collections to prevent basic memory corruption, but the real evil spot here
    // is the re-entrant loop - the state we are manipulating also sits inside the stack, and thus two racing threads can
    // get to the same part at the same time and start partying on this independently, which will lead to really bad things
    // in terms of the composition state of a part. This needs to be addressed if we have a prayer for this to be thread-safe
    public class CompositionEngine : IDisposable
    {
        private Dictionary<ComposablePart, ComposablePartState> _stateManager = new Dictionary<ComposablePart, ComposablePartState>();
        private List<ComposablePart> _recomposableParts = new List<ComposablePart>();
        private Stack<ComposablePartState> _partStack = new Stack<ComposablePartState>();

        private ExportProvider _sourceProvider;
        private Lock _lock = new Lock();
        private volatile bool _isDisposed = false;
        private volatile bool _isRunning = false;
        
        internal const int MaximumNumberOfCompositionIterations = 100;

        /// <summary>
        /// Initializes a new instance of the <see cref="CompositionEngine"/> class.
        /// </summary>
        public CompositionEngine()
        {
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!this._isDisposed)
                {
                    List<ComposablePart> partsToDispose = null;
                    bool disposeLock = false;
                    ExportProvider sourceProviderToUnsubscribeFrom = null;

                    try
                    {
                        using (new WriteLock(this._lock))
                        {
                            if (!this._isDisposed)
                            {
                                sourceProviderToUnsubscribeFrom = this._sourceProvider;
                                this._sourceProvider = null;

                                partsToDispose = this._recomposableParts;
                                this._recomposableParts = null;

                                this._partStack = null;
                                this._stateManager = null;
                                
                                disposeLock = true;
                                this._isDisposed = true;
                            }
                        }
                    }
                    finally
                    {
                        // All of this to is safe to do without a lock as only one thread gets to do it
                        if (sourceProviderToUnsubscribeFrom != null)
                        {
                            sourceProviderToUnsubscribeFrom.ExportsChanged -= this.OnExportsChanged;
                        }

                        // TODO - this is very wrong - the engine shouldn't own the parts
                        foreach (ComposablePart part in partsToDispose)
                        {
                            part.Dispose();
                        }

                        if (disposeLock)
                        {
                            this._lock.Dispose();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the source provider.
        /// </summary>
        /// <value>The source provider.</value>
        public ExportProvider SourceProvider
        {
            get
            {
                this.ThrowIfDisposed();

                return this._sourceProvider;
            }
            set
            {
                this.ThrowIfDisposed();

                Requires.NotNull(value, "value");

                using (new WriteLock(this._lock))
                {
                    this.EnsureCanSet(this._sourceProvider);
                    this._sourceProvider = value;
                }

                // This should be safe to do outside the lock, because only the first setter will ever win
                // and others will throw
                value.ExportsChanged += this.OnExportsChanged;
            }
        }

        private ComposablePart[] GetAffectedParts(IEnumerable<string> changedContractNames)
        {
            using (new ReadLock(this._lock))
            {
                return this._recomposableParts.Where(part =>
                {
                    // NOTE - if the import is something rather complex - that is, it doesn't have
                    // a single contract, we will have to mark the whole part for recomposition
                    // Otherwise we will only recompose it if necessary
                    return part.ImportDefinitions.Any(definition =>
                    {
                        if (!definition.IsRecomposable)
                        {
                            return false;
                        }

                        ContractBasedImportDefinition contractDefinition = definition as ContractBasedImportDefinition;
                        if (contractDefinition != null)
                        {
                            return changedContractNames.Contains(contractDefinition.ContractName, StringComparers.ContractName);
                        }

                        return true;
                    });
                }).ToArray();
            }
        }

        private void OnExportsChanged(object sender, ExportsChangedEventArgs e)
        {
            this.ThrowIfDisposed();
            this.EnsureRunning();

            var result = CompositionResult.SucceededResult;

            ComposablePart[] affectedParts = this.GetAffectedParts(e.ChangedContractNames);
            foreach (ComposablePart part in affectedParts)
            {
                result = result.MergeResult(this.SatisfyImports(part, true));
            }
            result.ThrowOnErrors();
        }

        /// <summary>
        /// Satisfies the imports and registers the part for recomposition.
        /// </summary>
        /// <param name="part">The part.</param>
        /// <returns></returns>
        public CompositionResult SatisfyImportsAndRegisterForRecomposition(ComposablePart part)
        {
            Requires.NotNull(part, "part");
            this.ThrowIfDisposed();
            this.EnsureRunning();

            return this.SatisfyImports(part, false);
        }

        /// <summary>
        /// Unregisters the part for recomposition.
        /// </summary>
        /// <param name="part">The part.</param>
        public void UnregisterForRecomposition(ComposablePart part)
        {
            Requires.NotNull(part, "part");
            this.ThrowIfDisposed();
            this.EnsureRunning();

            using (new WriteLock(this._lock))
            {
                this._stateManager.Remove(part);
                this._recomposableParts.Remove(part);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        private CompositionResult SatisfyImports(ComposablePart composablePart, bool recomposePostImports)
        {
            CompositionResult result = CompositionResult.SucceededResult;
            ComposablePartState partState = this.GetPartState(composablePart);

            try
            {
                int currentStackSize = 0;
                using (new WriteLock(this._lock))
                {
                    this._partStack.Push(partState);
                    currentStackSize = this._partStack.Count;
                }

                if (currentStackSize > CompositionEngine.MaximumNumberOfCompositionIterations)
                {
                    return result.MergeIssue(CompositionIssue.CreateIssueAsError(
                        CompositionIssueId.ComposeTookTooManyIterations,
                        Strings.ComposeTookTooManyIterations,
                        CompositionEngine.MaximumNumberOfCompositionIterations));
                }

                if (recomposePostImports && partState.State == CompositionState.AllImportsSatisfied)
                {
                    using (new WriteLock(this._lock))
                    {
                        this._recomposableParts.Remove(composablePart);
                        partState.State = CompositionState.NoImportsSatisfied;
                    }
                }

                while (partState.State != CompositionState.AllImportsSatisfied)
                {
                    switch (partState.State)
                    {
                        case CompositionState.NoImportsSatisfied:
                            {
                                partState.State = CompositionState.PreExportImportsSatisfying;

                                var preImports = composablePart.ImportDefinitions.Where(import => import.IsPrerequisite);

                                if (recomposePostImports)
                                {
                                    // It would be nice to actually we could actually filter this list to 
                                    // only imports that are recomposable and who have matching exports that
                                    // have changed but at this point the design doesn't easily support that.
                                    preImports = preImports.Where(import => import.IsRecomposable);
                                }

                                result = result.MergeResult(
                                    this.SatisfyImports(partState, composablePart, preImports));

                                if (!result.Succeeded)
                                {
                                    partState.State = CompositionState.NoImportsSatisfied;
                                    return result;
                                }
                                partState.State = CompositionState.PreExportImportsSatisfied;
                                break;
                            }
                        case CompositionState.PreExportImportsSatisfying:
                            {
                                var issue = CompositionIssue.CreateIssueAsError(
                                                CompositionIssueId.ImportCycle,
                                                Strings.ImportCycle);
                                return result.MergeIssue(issue);
                            }
                        case CompositionState.PreExportImportsSatisfied:
                            {
                                partState.State = CompositionState.PostExportImportsSatisfying;

                                var postImports = composablePart.ImportDefinitions.Where(import => !import.IsPrerequisite);

                                if (recomposePostImports)
                                {
                                    // It would be nice to actually we could actually filter this list to 
                                    // only imports that are recomposable and who have matching exports that
                                    // have changed but at this point the design doesn't easily support that.
                                    postImports = postImports.Where(import => import.IsRecomposable);
                                }

                                result = result.MergeResult(
                                    this.SatisfyImports(partState, composablePart, postImports));

                                if (!result.Succeeded)
                                {
                                    partState.State = CompositionState.PreExportImportsSatisfied;
                                    return result;
                                }
                                partState.State = CompositionState.PostExportImportsSatisfied;
                                break;
                            }
                        case CompositionState.PostExportImportsSatisfying:
                            {
                                // Ensure that no import in the cycle requires fully composed.
                                if (!this.AllPartsInCycleSatisfyCondition(partState, s => !s.RequiresFullyComposed))
                                {
                                    var issue = CompositionIssue.CreateIssueAsError(
                                                    CompositionIssueId.ImportCycle,
                                                    Strings.ImportCycle);
                                    return result.MergeIssue(issue);
                                }

                                // If no one in the cycle requires fully composed then simply return and allow 
                                // composition to continue. 
                                return result;
                            }
                        case CompositionState.PostExportImportsSatisfied:
                            {
                                result = result.MergeResult(composablePart.TryOnComposed());

                                using (new WriteLock(this._lock))
                                {
                                    this._recomposableParts.Add(composablePart);
                                }

                                partState.State = CompositionState.AllImportsSatisfied;
                                break;
                            }
                    }
                }
            }
            finally
            {
                using (new WriteLock(this._lock))
                {
                    ComposablePartState topState = this._partStack.Pop();
                    Assumes.IsTrue(partState == topState);
                }
            }

            return result;
        }

        private CompositionResult SatisfyImports(ComposablePartState partState, ComposablePart part, IEnumerable<ImportDefinition> imports)
        {
            CompositionResult result = CompositionResult.SucceededResult;

            foreach (ImportDefinition import in imports)
            {
                // Prerequisite's must be always fully composed
                partState.RequiresFullyComposed = import.IsPrerequisite;

                CompositionResult<IEnumerable<Export>> valueResult = CompositionServices.TryInvoke(() => this._sourceProvider.GetExports(import));
                result = result.MergeResult(valueResult.ToResultWithNoValue());

                if (!valueResult.Succeeded)
                {
                    continue;
                }

                result = result.MergeResult(part.TrySetImport(import, valueResult.Value));

                if (result.ContainsOnlyOneError(CompositionIssueId.ComposeTookTooManyIterations))
                {
                    // We are now in a degenerative state so remove this part and return failure now
                    this.UnregisterForRecomposition(part);
                    return result;
                }
            }

            partState.RequiresFullyComposed = false;
            return result;
        }

        private ComposablePartState GetPartState(ComposablePart composablePart)
        {
            ComposablePartState state;

            // TODO-MT - are we overlocking here?
            // We can of course acquire ReadLock, read, create the state, acquite WriteLock, read again and write
            // but that results in two reads from the dictionary. 
            // It's not clear, which is better - one WriteLock and a read or always two reads + ReadLock
            using (new WriteLock(this._lock))
            {
                if (!this._stateManager.TryGetValue(composablePart, out state))
                {
                    state = new ComposablePartState();
                    this._stateManager.Add(composablePart, state);
                }
            }
            return state;
        }

        private bool AllPartsInCycleSatisfyCondition(ComposablePartState state, Predicate<ComposablePartState> condition)
        {
            Assumes.NotNull(state);
            Assumes.IsTrue(this._partStack.First() == state);

            ComposablePartState lastState = null;

            foreach (ComposablePartState partState in this._partStack.Skip(1))
            {
                if (!condition(partState))
                {
                    return false;
                }

                if (partState == state)
                {
                    lastState = partState;
                    break;
                }
            }

            Assumes.IsTrue(lastState == state);
            return true;
        }

        [DebuggerStepThrough]
        private void ThrowIfDisposed()
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException(this.GetType().ToString());
            }
        }

        [DebuggerStepThrough]
        private void EnsureCanRun()
        {
            if (this._sourceProvider == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.ObjectMustBeInitialized, "SourceProvider")); // NOLOC
            }
        }

        [DebuggerStepThrough]
        private void EnsureRunning()
        {
            if (!this._isRunning)
            {
                using (new WriteLock(this._lock))
                {
                    if (!this._isRunning)
                    {
                        this.EnsureCanRun();
                        this._isRunning = true;
                    }
                }
            }
        }

        [DebuggerStepThrough]
        private void EnsureCanSet<T>(T currentValue)
            where T : class
        {
            if ((this._isRunning) || (currentValue != null))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.ObjectAlreadyInitialized));
            }
        }

        private enum CompositionState
        {
            NoImportsSatisfied,
            PreExportImportsSatisfying,
            PreExportImportsSatisfied,
            PostExportImportsSatisfying,
            PostExportImportsSatisfied,
            AllImportsSatisfied
        }

        private class ComposablePartState
        {
            public CompositionState State { get; set; }
            public bool RequiresFullyComposed { get; set; }
        }
    }
}
